package org.bookshare.document.beans;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.benetech.collections.ListMap;
import org.benetech.event.EventListener;
import org.benetech.util.EncodingUtils;
import org.benetech.util.FileUtils;
import org.benetech.util.HTMLUtils;
import org.benetech.util.MapUtils;
import org.benetech.util.ViolationCollatingErrorHandler;
import org.benetech.util.XMLChangeUtils;
import org.jdom.Document;
import org.jdom.JDOMException;

public abstract class AbstractDocumentSet<T extends DocumentType> implements DocumentSet<T> {
    private ListMap<DocumentComponent, Document> documentMap = new ListMap<DocumentComponent, Document>();
    private ListMap<DocumentComponent, File> fileMap = new ListMap<DocumentComponent, File>();
    private Map<DocumentComponent, Boolean> hasBeenParsed = new HashMap<DocumentComponent, Boolean>();
    private Map<File, Document> documentFileMap = new HashMap<File, Document>();
    private File fileBasePath;
    private T documentType;
    private ViolationCollatingErrorHandler errorHandler;
    private EventListener eventListener;
    private boolean outputFormat;
    private DocumentWrapperFactory factory;

    /**
     * The base path of files that are included in this set.
     * @param fileBasePath The basepath of the files
     * @param documentType The type of document being represented
     * @param eventListener The event listener
     * @param errorHandler The error handler
     * @param outputFormat True if this is an output format - this turns off unicode stripping
     */
    public AbstractDocumentSet(final File fileBasePath, final DocumentType documentType, final EventListener eventListener,
    		final ViolationCollatingErrorHandler errorHandler, final boolean outputFormat)
    {
    	this.fileBasePath = fileBasePath;
    	this.documentType = (T) documentType;
    	this.errorHandler = errorHandler;
    	this.eventListener = eventListener;
    	this.outputFormat = outputFormat;
    }

    /**
     * {@inheritDoc}
     */
    public T getType() {
    	return documentType;
    }

    /**
     * {@inheritDoc}
     */
   public final Document getDocument(final DocumentComponent docElement) {
	   final List<Document> documents = getDocuments(docElement);
	   if (documents.size() > 0) {
		   return documents.get(0);
	   }
	   return null;
   }

   /**
    * {@inheritDoc}
    */
	public final List<Document> getDocuments(final DocumentComponent docElement) {
		final List<Document> documents = documentMap.get(docElement);
		if (documents == null) {
			return new ArrayList<Document>();
		}
		return documents;
	}

	/**
	 * {@inheritDoc}
	 */
	public final File getFile(final DocumentComponent docElement) {
		final List<File> files = getFiles(docElement);
		if (files.size() > 0) {
			return files.get(0);
		}
		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	public final List<File> getFiles(final DocumentComponent docElement) {
		final List<File> files = fileMap.get(docElement);
		if (files == null) {
			return new ArrayList<File>();
		}
		return files;
	}

	/**
	 * {@inheritDoc}
	 */
	public final void addFiles(final DocumentComponent docElement, final File... files)
	throws IOException
	{
		for (File file : files) {
			fileMap.put(docElement, file);

			// we may need to alter the contents at a raw level...
			if (!outputFormat && docElement.getAttributes() != null
					&& docElement.getAttributes().contains(DocumentComponent.ComponentAttribute.XML))
			{
				getEventListener().message("Stripping unicode escape sequences");
				// strip out any unicode escape sequences
				final String fileContents = EncodingUtils.stripEscapedUnicode(FileUtils.getFileAsString(file, "\n"),
						getEventListener());
				FileUtils.writeToFile(file, fileContents.getBytes());
				// XXX we may want to offer different kinds of doctypes. note that this method also trims crap
				// out that may exist before the <?xml header
				getEventListener().message("Cleaning header");
				HTMLUtils.addDocTypeIfNonexistent(file);
			}
			if (!outputFormat && docElement.getAttributes() != null
				&& docElement.getAttributes().contains(DocumentComponent.ComponentAttribute.TEXT))
			{
				getEventListener().message("Stripping unicode characters");
				// strip out unicode characters
				final String fileContents = EncodingUtils.stripUnicodeChars(FileUtils.getFileAsString(file, "\n"),
						getEventListener());
				FileUtils.writeToFile(file, fileContents.getBytes());
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public final void addFiles(final DocumentComponent docElement, final List<File> files)
	throws IOException
	{
		for (File file : files) {
			addFiles(docElement, file);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public final void overwriteFiles(final DocumentComponent docElement, final File... files)
	throws IOException
	{
		for (File file : fileMap.get(docElement)) {
			documentFileMap.remove(file);
		}
		fileMap.remove(docElement);
		documentMap.remove(docElement);
		addFiles(docElement, files);
	}

    /**
     * {@inheritDoc}
     */
    public final File getFile(final Document document) {
       final List<File> files = new MapUtils<File, Document>().reverseMap(documentFileMap, document);
       if (files != null && files.size() > 0) {
           return files.get(0);
       }
       return null;
    }

    /**
     * {@inheritDoc}
     */
    public final Document getDocument(final File file) {
        return documentFileMap.get(file);
    }

    /**
     * {@inheritDoc}
     */
    public final Document getDocument(final String href) {
    	String name;
    	if (href.contains("/")) {
    		name = href.substring(href.lastIndexOf("/") + 1, href.length());
    	} else if (href.contains("\\")) {
    		name = href.substring(href.lastIndexOf("\\") + 1, href.length());
    	} else {
    		name = href;
    	}

    	for (File file : documentFileMap.keySet()) {
    		if (file.getName().endsWith(name)) {
    			return documentFileMap.get(file);
    		}
    	}
    	return null;
    }

    /**
     * {@inheritDoc}
     */
    public final boolean hasBeenParsed(final DocumentComponent docElement) {
    	final Boolean b = hasBeenParsed.get(docElement);
        if (b == null) {
            return false;
        }
        return b;
    }

    /**
     * {@inheritDoc}
     */
    public final void createDocuments(final DocumentComponent docElement, final boolean reparse)
    	throws JDOMException, IOException
    {
    	if (reparse || !hasBeenParsed(docElement)) {
	    	final Map<File, Document> parseResults =
	    		FileUtils.parseAndValidate(getFiles(docElement), errorHandler, eventListener);
	        documentMap.putAll(docElement, parseResults.values());
	        for (Map.Entry<File, Document> result : parseResults.entrySet()) {
	            documentFileMap.put(result.getKey(), result.getValue());
	        }
	        hasBeenParsed.put(docElement, true);
    	}
    }

    /**
     * Get the base path of the files.
     * @return never null
     */
    public final File getFileBasePath() {
        return fileBasePath;
    }

    /**
     * {@inheritDoc}
     */
    public final DocumentComponent[] getDocumentElements() {
    	return documentType.getComponents();
    }

    /**
     * Return the error handler.
     * @return Never null
     */
    protected final ViolationCollatingErrorHandler getErrorHandler() {
    	return errorHandler;
    }

    /**
     * Return the event listener.
     * @return Never null
     * @return
     */
    protected final  EventListener getEventListener() {
    	return eventListener;
    }

    /**
     * Return the document wrapper factory for creating of specialized beans.
     * @return Never null.
     */
    protected final DocumentWrapperFactory getFactory() {
    	if (factory == null) {
    		factory = new DocumentWrapperFactory(this);
    	}
    	return factory;
    }
}
